﻿using BlockeyEssentials;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using BlockeyEssentials.Input;
using Microsoft.Xna.Framework.Input;

namespace Blockey.GameObjects3D
{
    public class Camera3D
    {
        public Camera3D()
        {
            m_fAzimuthCurrent = 0.0f;
            m_fAzimuth = 0.0f;
            m_fAzimuthLerpDistance = 0.0f;

            m_fElevationCurrent = 0.0f;
            m_fElevation = 0.643f;
            m_fElevationLerpDistance = 0.0f;

            m_fZoomCurrent = 0.0f;
            m_fZoom = 75.0f;
            m_fZoomLerpDistance = 0.0f;

            m_vPosition = Vector3.Zero;
            m_vTargetCurrent = Vector3.Zero;
            m_vTarget = new Vector3(0, -3f, 0);
            m_fTargetLerpDistance = 0.0f;

            m_oFrustrum = new BoundingFrustum(Matrix.Identity);
            m_qRotation = Quaternion.Identity;

            m_mWorld = Matrix.Identity;
            m_mView = Matrix.Identity;
            m_mProjection = Matrix.Identity;
        }

        #region Update

        public void Update()
        {
            LerpTransformation();
            UpdateProjectionMatrix();
        }

        public void UpdateViewMatrix()
        {
            m_qRotation =
                Quaternion.CreateFromAxisAngle(Vector3.UnitZ, m_fAzimuthCurrent) *
                Quaternion.CreateFromAxisAngle(Vector3.UnitX, m_fElevationCurrent);

            m_vPosition = Vector3.UnitZ * m_fZoomCurrent;
            m_vPosition = Vector3.Transform(m_vPosition, m_qRotation);
            m_vPosition += m_vTargetCurrent;

            m_mView = Matrix.CreateLookAt(m_vPosition, m_vTargetCurrent, Vector3.UnitZ);
            m_oFrustrum = new BoundingFrustum(m_mView * m_mProjection);
        }
        public void UpdateProjectionMatrix()
        {
            m_mProjection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.PiOver4,
                All.Graphics.GraphicsDevice.Viewport.AspectRatio,
                g_fViewNear,
                g_fViewFar);
        }

        #endregion

        #region Lerp Methods

        private void LerpTransformation()
        {
            m_fZoom = MathHelper.Clamp(m_fZoom, g_fMinZoom, g_fMaxZoom);
            bool bLerped = false;

            // Target
            LerpValue(ref m_vTargetCurrent, ref m_vTarget, ref m_fTargetLerpDistance, g_fMinTargetLerp, ref bLerped);

            // Azimuth
            LerpValue(ref m_fAzimuthCurrent, ref m_fAzimuth, ref m_fAzimuthLerpDistance, g_fMinTargetLerp, ref bLerped);

            // Elevation
            LerpValue(ref m_fElevationCurrent, ref m_fElevation, ref m_fElevationLerpDistance, g_fMinTargetLerp, ref bLerped);

            // Zoom
            LerpValue(ref m_fZoomCurrent, ref m_fZoom, ref m_fZoomLerpDistance, g_fMinZoomLerp, ref bLerped);

            // Updates View Matrix if something changed
            if (bLerped)
            {
                UpdateViewMatrix();
            }
        }
        private void LerpValue(ref Vector3 oCurrent, ref Vector3 oDestination, ref float oDistance, float oLerp, ref bool bChanged)
        {
            if (oDestination != oCurrent)
            {
                oCurrent = Vector3.Lerp(oCurrent, oDestination, g_fLerp);
                oDistance = Vector3.Distance(oCurrent, oDestination);

                if (oDistance <= g_fMinTargetLerp)
                {
                    oCurrent = oDestination;
                    oDistance = 0;
                }

                bChanged = true;
            }
        }
        private void LerpValue(ref float oCurrent, ref float oDestination, ref float oDistance, float oLerp, ref bool bChanged)
        {
            if (oDestination != oCurrent)
            {
                oCurrent = MathHelper.Lerp(oCurrent, oDestination, g_fLerp);
                oDistance = MathHelper.Distance(oCurrent, oDestination);

                if (oDistance <= oLerp)
                {
                    oCurrent = oDestination;
                    oDistance = 0;
                }

                bChanged = true;
            }
        }

        #endregion

        public void Input()
        {
            var oDirection = Vector3.Zero;

            var oInputKey = InputManager.Keyboard;
            bool oShift = oInputKey.IsPressedOne(Keys.LeftShift, Keys.RightShift);
            bool oControl = oInputKey.IsPressedOne(Keys.LeftControl, Keys.RightControl);
            bool oZ = oInputKey.IsPressedOne(Keys.Z);
            bool oUp = oInputKey.IsPressedOne(Keys.Up);
            bool oDown = oInputKey.IsPressedOne(Keys.Down);
            bool oLeft = oInputKey.IsPressedOne(Keys.Left);
            bool oRight = oInputKey.IsPressedOne(Keys.Right);

            // Specific angles
            if (oZ)
            {
                if (oUp)
                {
                    m_fAzimuth = -MathHelper.PiOver2;
                }
                if (oDown)
                {
                    m_fAzimuth = MathHelper.PiOver2;
                }
                if (oRight)
                {
                    m_fAzimuth = 0;
                }
                if (oLeft)
                {
                    m_fAzimuth = MathHelper.Pi;
                }
            }

            // Custom Angles
            else
            {
                if (oShift)
                {
                    if (oUp)
                    {
                        m_fZoom -= g_fZoomRate;
                    }
                    if (oDown)
                    {
                        m_fZoom += g_fZoomRate;
                    }
                }
                else if (oControl)
                {
                    if (oUp)
                    {
                        m_fElevation -= g_fRotationRate;
                    }
                    if (oDown)
                    {
                        m_fElevation += g_fRotationRate;
                    }
                    if (oRight)
                    {
                        m_fAzimuth += g_fRotationRate;
                    }
                    if (oLeft)
                    {
                        m_fAzimuth -= g_fRotationRate;
                    }
                }
                else
                {
                    if (oUp) oDirection.Y += 1;
                    if (oDown) oDirection.Y -= 1;
                    if (oRight) oDirection.X += 1;
                    if (oLeft) oDirection.X -= 1;
                }
            }

            var oInputGamepad = InputManager.GamePad1;

            m_fZoom -= g_fZoomRate * oInputGamepad.LeftTrigger;
            m_fZoom += g_fZoomRate * oInputGamepad.RightTrigger;

            m_fAzimuth += g_fRotationRate * oInputGamepad.RightStick.X;
            m_fElevation += g_fRotationRate * oInputGamepad.RightStick.Y * -1;

            if (oDirection == Vector3.Zero)
                oDirection = new Vector3(oInputGamepad.LeftStick, 0);

            if (oDirection != Vector3.Zero)
            {
                oDirection.Normalize();
                FlatDirectionRelativeToCamera(ref oDirection);
                m_vTarget +=
                    oDirection
                    * g_fTranslateRate
                    * m_fZoomCurrent * 0.05f;
            }
        }

        public Vector3 FlatDirectionRelativeToCamera(ref Vector3 oDirectionalInput)
        {
            oDirectionalInput = Vector3.Transform(oDirectionalInput, m_qRotation);
            oDirectionalInput.Z = 0;
            oDirectionalInput.Normalize();

            return oDirectionalInput;
        }

        public Vector3 Direction()
        {
            if (m_vPosition != m_vTargetCurrent)
            {
                return Vector3.Normalize(m_vTargetCurrent - m_vPosition);
            }
            return Vector3.Zero;
        }

        public void SetTarget(Vector3 oNewTarget)
        {
            m_vTarget = oNewTarget;
        }
        public void SetAzimuth(float oNewAzimuth)
        {
            m_fAzimuth = oNewAzimuth;
        }
        public void SetElevation(float oNewElevation)
        {
            m_fElevation = oNewElevation;
        }
        public void SetZoom(float oNewZoom)
        {
            m_fZoom = oNewZoom;
        }

        public void ApplyCameraEffect(ref BasicEffect oBasicEffect)
        {
            oBasicEffect.View = m_mView;
            oBasicEffect.Projection = m_mProjection;
        }

        public Matrix View
        {
            get { return m_mView; }
        }

        public Matrix Projection
        {
            get { return m_mProjection; }
        }

        public float Zoom
        {
            get { return m_fZoomCurrent; }
        }

        public Vector3 Target
        {
            get { return m_vTarget; }
        }

        #region Fields

        private float m_fAzimuthCurrent;
        private float m_fAzimuth;
        private float m_fAzimuthLerpDistance;

        private float m_fElevationCurrent;
        private float m_fElevation;
        private float m_fElevationLerpDistance;

        private float m_fZoomCurrent;
        private float m_fZoom;
        private float m_fZoomLerpDistance;

        private Vector3 m_vPosition;
        private Vector3 m_vTargetCurrent;
        private Vector3 m_vTarget;
        private float m_fTargetLerpDistance;

        private BoundingFrustum m_oFrustrum;
        private Quaternion m_qRotation;

        private Matrix m_mWorld;
        private Matrix m_mView;
        private Matrix m_mProjection;

        #endregion

        #region Constants

        private const float g_fViewNear = 1f;
        private const float g_fViewFar = 400f;
        private const float g_fDegrees45 = 0.7853f;
        private const float g_fLerp = 0.2f;
        private const float g_fMinTargetLerp = 0.005f;
        private const float g_fMinRotationLerp = 0.005f;
        private const float g_fMinZoomLerp = 0.05f;
        private const float g_fMinZoom = 0.5f;
        private const float g_fMaxZoom = 300f;
        private const float g_fRotationRate = 0.0314f;
        private const float g_fZoomRate = 0.75f;
        private const float g_fTranslateRate = 0.4f;

        #endregion

    }
}
